using System;
using System.Collections.Generic;
using System.IO;

namespace DeflateLib
{
    /// <summary>
    /// Implementations of the deflate stream. 
    /// </summary>
    public class DeflateStream : Stream
    {
        const int BufferSize = 0x2000;

        Stream baseStream;
        CompressionMode mode;
        bool leaveOpen;
        bool isOpen;

        /// <summary>
        /// Gets base data stream.
        /// </summary>
        public Stream BaseStream
        {
            get { return baseStream; }
        }

        /// <summary>
        /// Gets stream mode: compression or decompression.
        /// </summary>
        public CompressionMode Mode
        {
            get { return mode; }
        }

        Encoder encoder = null;
        Decoder decoder = null;
        byte[] internalBuffer;
        int internalBufferLength = 0;

        /// <summary>
        /// Creates deflate stream.
        /// </summary>
        /// <param name="baseStream">The stream that contains compress/decompressed data.</param>
        /// <param name="mode">Specifies compression or decompression mode</param>
        public DeflateStream(Stream baseStream, CompressionMode mode)
            : this(baseStream, mode, false)
        {
        }

        /// <summary>
        /// Creates deflate stream.
        /// </summary>
        /// <param name="baseStream">The stream that contains compress/decompressed data.</param>
        /// <param name="mode">Specifies compression or decompression mode</param>
        /// <param name="leaveOpen">Specifies if base must be kept open after closing this stream</param>
        public DeflateStream(Stream baseStream, CompressionMode mode, bool leaveOpen)
        {
            if (baseStream == null) throw new ArgumentNullException("baseStream");

            this.baseStream = baseStream;
            this.mode = mode;
            this.leaveOpen = leaveOpen;
            this.isOpen = true;

            switch (mode)
            {
                case CompressionMode.Compress:
                    internalBuffer = new byte[BufferSize];
                    encoder = new Encoder(baseStream);
                    break;
                case CompressionMode.Decompress:
                    decoder = new Decoder(baseStream);
                    break;
            }
        }

        /// <summary>
        /// Returns false - this stream cannot seek.
        /// </summary>
        public override bool CanSeek
        {
            get { return false; }
        }

        /// <summary>
        /// Returns true if decompression mode was set, otherwise false.
        /// </summary>
        public override bool CanRead
        {
            get { return mode == CompressionMode.Decompress; }
        }

        /// <summary>
        /// Returns true if compression mode was set, otherwise false.
        /// </summary>
        public override bool CanWrite
        {
            get { return mode == CompressionMode.Compress; }
        }

        /// <summary>
        /// Raises exception - cannot return length.
        /// </summary>
        public override long Length
        {
            get { throw new NotImplementedException(); }
        }

        /// <summary>
        /// Raises exception - cannot return current position.
        /// </summary>
        public override long Position
        {
            get { throw new NotImplementedException(); }
            set { throw new NotImplementedException(); }
        }

        /// <summary>
        /// Raises exception - cannot seek.
        /// </summary>
        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Raises exception - cannot set length.
        /// </summary>
        public override void SetLength(long value)
        {
            throw new NotImplementedException();
        }

        private void EnsureOpen()
        {
            if(!isOpen)
                throw new InvalidOperationException("Stream is closed");
        }

        private void EnsureCompression()
        {
            EnsureOpen();
            if(mode != CompressionMode.Compress)
                throw new InvalidOperationException("This operation is not available for compression");
        }

        private void EnsureDecompression()
        {
            EnsureOpen();
            if (mode != CompressionMode.Decompress)
                throw new InvalidOperationException("This operation is not available for decompression");
        }

        /// <summary>
        /// Reads data like all streams. If compressed stream contains 
        /// sych blocks, the method return incomplete buffer.
        /// </summary>
        /// <param name="buffer">Buffer for data</param>
        /// <param name="offset">Buffer offset</param>
        /// <param name="count">Bytes count to be read</param>
        /// <returns>Amount read bytes</returns>
        public override int Read(byte[] buffer, int offset, int count)
        {
            if (count < 0 && count > buffer.Length) throw new ArgumentOutOfRangeException("count");
            if (offset < 0 && offset > buffer.Length) throw new ArgumentOutOfRangeException("offset");
            if (offset + count > buffer.Length) throw new ArgumentException("offset + count out of buffer");

            EnsureDecompression();

            if (count > 0)
            {
                return decoder.Read(buffer, offset, count);
            }
            else
                return 0;
        }

        /// <summary>
        /// Writes data as normal stream.
        /// </summary>
        /// <param name="buffer">Buffer for data</param>
        /// <param name="offset">Buffer offset</param>
        /// <param name="count">Bytes count to be written</param>
        public override void Write(byte[] buffer, int offset, int count)
        {
            if (count < 0 && count > buffer.Length) throw new ArgumentOutOfRangeException("count");
            if (offset < 0 && offset > buffer.Length) throw new ArgumentOutOfRangeException("offset");
            if (offset + count > buffer.Length) throw new ArgumentException("offset + count out of buffer");

            EnsureCompression();

            if (internalBufferLength + count > BufferSize)
            {                
                FlushInternalBuffer();
            }

            if (count >= BufferSize)
            {
                System.Diagnostics.Debug.Assert(internalBufferLength == 0);

                int i = 0;
                while (count - i > Encoder.MaxEncodeLength)
                {
                    encoder.Encode(buffer, offset + i, Encoder.MaxEncodeLength, false);
                    i += Encoder.MaxEncodeLength;
                }
                encoder.Encode(buffer, offset + i, count - i, false);
            }
            else
            {
                System.Diagnostics.Debug.Assert(internalBufferLength + count <= BufferSize);

                // combine small chunks
                Array.Copy(buffer, offset, internalBuffer, internalBufferLength, count);
                internalBufferLength += count;

                if (internalBufferLength == BufferSize)
                {
                    FlushInternalBuffer();
                }
            }
        }

        private void FlushInternalBuffer()
        {
            if (internalBufferLength > 0)
            {
                encoder.Encode(internalBuffer, 0, internalBufferLength, false);
                internalBufferLength = 0;
            }
        }

        /// <summary>
        /// Creates aligned sync block and flushes data.
        /// </summary>
        public override void Flush()
        {
            EnsureCompression();

            if (internalBufferLength > 0)
            {
                FlushInternalBuffer();
            }
            encoder.WriteSyncBlock(true, false);

            BaseStream.Flush();
        }

        private void FlushLastData()
        {
            if (internalBufferLength > 0)
            {
                encoder.Encode(internalBuffer, 0, internalBufferLength, true);
            }
            else
            {
                encoder.WriteSyncBlock(false, true);
            }
        }

        /// <summary>
        /// Disposes the stream.
        /// </summary>
        /// <param name="disposing">true if called for Dispose.</param>
        protected override void Dispose(bool disposing)
        {
            if (isOpen)
            {
                switch(mode)
                {
                    case CompressionMode.Compress:
                        FlushLastData();
                        internalBuffer = null;
                        encoder = null;
                        break;
                    case CompressionMode.Decompress:
                        if (leaveOpen)
                        {
                            decoder.SkipToEnd();
                        }
                        decoder = null;
                        break;
                }

                if (!leaveOpen)
                {
                    baseStream.Close();
                    baseStream = null;
                }
                isOpen = false;
            }
            base.Dispose(disposing);
        }
    }

    /// <summary>
    /// Specifies DeflateStream mode.
    /// </summary>
    public enum CompressionMode
    {
        /// <summary>
        /// Data will be compressed.
        /// </summary>
        Compress,
        /// <summary>
        /// Data will be decompressed.
        /// </summary>
        Decompress
    }
}
